﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Threading;
using System.Xml;

using NetworkManagement;
using LorraineSec.Libraries.Sensors;
using LorraineSec.MessageLib;
using LorraineSec.MessageLib.Messages;
using LorraineSec.DatabaseLib;
using LorraineSec.DatabaseLib.DataObjects;

namespace LorraineSec.ServerApplication
{
    /// <summary>
    /// The main server program responsible for managing sensors, interfacing with the
    /// database, providing a subscription interface and managing subscribed clients,
    /// performing logic for requests.
    /// <remarks>
    /// 
    /// </remarks>
    /// </summary>
    internal sealed class ServerApplication : Server
    {
        #region Enums / Constants / Instance Variables

        /// <summary>
        /// A constant defining the max number of pending connections allowed.
        /// </summary>
        private const int MAX_PENDING = 1000;

        /// <summary>
        /// A constant defining the timeout period for recieving a connection in ms.
        /// </summary>
        private const int RECV_TIMEOUT = 1000;

        /// <summary>
        /// A constant for outputing a line of dashes.
        /// </summary>
        private const string OUTPUT_LINE = "--------------------------------------------------";

        /// <summary>
        /// Signal object to notify that a client connection has been accepted.
        /// </summary>
        private ManualResetEventSlim ClientAccepted;

        /// <summary>
        /// Signal object to notify the listening thread to shutdown.
        /// </summary>
        private ManualResetEventSlim ServerShutdown;

        /// <summary>
        /// The IPEndPoint for the local server.
        /// </summary>
        private IPEndPoint IPEnd;

        /// <summary>
        /// The object responsible for assembling messages from byte arrays passed on by a message stream.
        /// </summary>
        private MessageFactory MsgFac;

        /// <summary>
        /// The listening socket used for accepting incoming connections to the server.
        /// </summary>
        public Socket ServerListen { get; private set; }

        /// <summary>
        /// A list of all the active connections with the server
        /// </summary>
        public LinkedList<ServerConnection> Clients;

        /// <summary>
        /// The database object which handles the construction and maintenance of the database.
        /// </summary>
        public XmlDatabase DataBase { get; private set; }

        /// <summary>
        /// A dictionary of DataSensors Key-Indexed by DeviceSerial#
        /// </summary>
        public Dictionary<string, Sensor> Sensors;

        /// <summary>
        /// A dictionary mapping Sensors to Devices
        /// </summary>
        public Dictionary<Sensor, Device> SensorDeviceMap;

        /// <summary>
        /// A dictionary of a list ServerConnections Key-Indexed by the Sensor Library they are subscribed to.
        /// </summary>
        public Dictionary<Sensor, List<ServerConnection>> SensorSubs;

        /// <summary>
        /// Adictionary of a list of ServerConnections Key-Indexed by the Compartment ID they are subscribed to.
        /// </summary>
        public Dictionary<uint, List<ServerConnection>> CompartmentSubs;
        #endregion

        public ServerApplication(IPAddress listenIP, int port, ServerOutputCallback cb) : base(cb)
        {
            IPEnd = new IPEndPoint(listenIP, port);
            ServerListen = new Socket(IPEnd.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            ClientAccepted = new ManualResetEventSlim(false);
            ServerShutdown = new ManualResetEventSlim(false);
            
            DataBase = new XmlDatabase();
            MsgFac = new LorraineSecMessageFactory();

            Clients = new LinkedList<ServerConnection>();
            Sensors = new Dictionary<string, Sensor>();
            SensorDeviceMap = new Dictionary<Sensor, Device>();
            SensorSubs = new Dictionary<Sensor, List<ServerConnection>>();
            CompartmentSubs = new Dictionary<uint, List<ServerConnection>>();
        }

        #region Event Handlers
        public void SensorRecvCallback(SensorFrame frame)
        {
            //Filter Non-Data related messages
            if (frame.Data[0].CompareTo(Sensor.TYPE_DELIMETER) != 0)
            {
                //Loop through all subscribed clients
                foreach (ServerConnection con in SensorSubs[frame.Sender])
                {
                    con.SensorRecvCallback(frame);
                }
            }
        }
        #endregion

        #region Core Visible Functions
        public bool init(bool createDB, string filepath)
        {
            //Load Database
            if (createDB)
            {
                output(OutputFlag.TEXT, "Initializing Server Application with new Database: '" + filepath + "'.");
                DataBase.createNew(filepath);
            }
            else
            {
                output(OutputFlag.TEXT, "Initializing Server Application with loaded Database: '" + filepath + "'.");
                DataBase.load(filepath);
            }

            //Startup Sensor Libraries
            foreach (Device dev in Device.GetList(DataBase))
            {
                //Device loading not currently implemented
            }

            return true;
        }

        /// <summary>
        /// Starts the Server Application by running the listening thread and accepting incoming connections.
        /// </summary>
        /// <returns>True is startup was successful. False if unsuccessful.</returns>
        public override bool start()
        {
            if (serverStatus == Status.IDLE)
            {
                output(OutputFlag.TEXT, "Starting Server Application.");
                Thread listenThread = new Thread(new ThreadStart(listen));
                listenThread.Name = "Auth-GUIClientListenThread";
                listenThread.Start();
                output(OutputFlag.TEXT, OUTPUT_LINE);
                return true;
            }
            output(OutputFlag.TEXT, "ERROR: Server already running!");
            return false;
        }

        /// <summary>
        /// Stops the Server Application by halting the listening thread and dropping all current connections.
        /// </summary>
        /// <returns>True if shutdown was successful. False if unsuccessful.</returns>
        public override bool stop()
        {
            if (serverStatus == Status.RUNNING)
            {
                output(OutputFlag.TEXT, "Shutting down Server Application.");
                //Reset shutdown signal
                ServerShutdown.Reset();
                //Set the status flag to STOPPING
                serverStatus = Status.STOPPING;
                //Signal the Listen Thread to unblock
                ClientAccepted.Set();
                //Block until listen thread signals
                ServerShutdown.Wait();

                //Save Database
                DataBase.save();

                //Set the status flag to IDLE 
                serverStatus = Status.IDLE;
                output(OutputFlag.TEXT, "Shutdown successful. Restart Application to start Server again.");
                return true;
            }
            output(OutputFlag.TEXT, "ERROR: Server not currently running!");
            return false;
        }
        
        /// <summary>
        /// Waits for connection attempts and handles them accordingly.
        /// </summary>
        public void listen()
        {
            //Set the status to "Running"
            serverStatus = Status.RUNNING;
            //Tell the ServerConnection class that this is the server.
            ServerConnection.ServApp = this;

            ServerListen.Bind(IPEnd);
            ServerListen.Listen(MAX_PENDING);
            //Loop while the server should be running
            while (serverStatus == Status.RUNNING)
            {
                //Reset the thread signal
                ClientAccepted.Reset();

                //Accep the new client and start a handler thread
                ServerListen.BeginAccept(new AsyncCallback(handleConnection), ServerListen);
            
                //Wait until other thread signals it has otained the connection
                ClientAccepted.Wait();
            }

            //Close the listening socket. Should not disconnect since it is a listening socket.
            output(OutputFlag.TEXT, "Closing listening socket.");
            ServerListen.Close();

            //Signal stop function
            ServerShutdown.Set();
        }
        #endregion

        #region Connection Handling
        private void handleConnection(IAsyncResult ar)
        {
            /* Check to make sure the server is still running.
             * Since we cannot abort a BeginAccept() call we must
             * check if the server is running before attempting
             * to call EndAccept()
             */
            if (serverStatus == Status.RUNNING)
            {
                //Signal listen thread to continue
                ClientAccepted.Set();

                //Accept the new connection
                Socket listener = (Socket)ar.AsyncState;
                Socket newSocket = listener.EndAccept(ar);

                //Create a new client connection
                output(OutputFlag.TEXT, "Accepted new client from: " + newSocket.RemoteEndPoint.ToString());
                ServerConnection thisClient = new ServerConnection();
                LinkedListNode<ServerConnection> clientNode = Clients.AddLast(thisClient);
                thisClient.msgInt = new MessageStreamInterface(newSocket);
                thisClient.status = ServerConnection.Status.CONNECTED;

                //Main Connection Loop
                Message newMsg;
                bool connectionOpen = true;
                while (connectionOpen)
                {
                    //Get next message from socket
                    newMsg = MsgFac.buildMessage(thisClient.msgInt.getNextMessage());
                    //If the message was valid
                    if (newMsg != null)
                        //Handle the message
                        connectionOpen = handleMessage(thisClient, newMsg);
                    else
                        //Message Invalid: Close Connection
                        connectionOpen = false;
                }
             
                //Disconnect the client
                output(OutputFlag.TEXT, "Disconnecting from client: " + newSocket.RemoteEndPoint.ToString());
                Clients.Remove(clientNode);
                disconnectSocket(newSocket);
            }
        }
        #endregion

        #region Message Handling
        /// <summary>
        /// Handles the given message by delegating to the correct function call on the connection object.
        /// </summary>
        /// <param name="cln">The current client of this connection.</param>
        /// <param name="msg">The recieved message in a base format.</param>
        /// <returns>True if connection should continue. False if connection should terminate.</returns>
        private bool handleMessage(ServerConnection cln, Message msg)
        {
            switch ((LorraineSecMessageFactory.MessageID)msg.MessageID)
            {
                case LorraineSecMessageFactory.MessageID.KEY_REQUEST:
                    return cln.handle_KeyRequest(PublicKey, msg as KeyRequestMessage);
                case LorraineSecMessageFactory.MessageID.SYMKEY_EXCHANGE:
                    return cln.handle_SymKeyExchange(PublicKey, msg as SymKeyExchangeMessage);
                case LorraineSecMessageFactory.MessageID.AUTH_REQUEST:
                    return cln.handle_AuthRequest(msg as AuthRequestMessage);
                case LorraineSecMessageFactory.MessageID.COMPARTMENT_LIST_REQUEST:
                    return cln.handle_CompListRequest(msg as CompartmentListRequest);
                case LorraineSecMessageFactory.MessageID.ASSET_LIST_REQUEST:
                    return cln.handle_AssetListRequest(msg as AssetListRequest);
                case LorraineSecMessageFactory.MessageID.LOG_LIST_REQUEST:
                    return cln.handle_LogListRequest(msg as LogListRequest);
                case LorraineSecMessageFactory.MessageID.DEVICE_LIST_REQUEST:
                    return cln.handle_DeviceListRequest(msg as DeviceListRequest);
                case LorraineSecMessageFactory.MessageID.SUBSCRIBE_COMPARTMENT_REQUEST:
                    return cln.handle_SubscribeCompartmentRequest(msg as SubscribeCompartmentRequest);
                case LorraineSecMessageFactory.MessageID.SUBSCRIBE_DEVICE_REQUEST:
                    return cln.handle_SubscribeDeviceRequest(msg as SubscribeDeviceRequest);
                case LorraineSecMessageFactory.MessageID.CREATE_LOG:
                    return cln.handle_CreateLog(msg as CreateLogMessage);
                case LorraineSecMessageFactory.MessageID.EDIT_ASSET:
                    return cln.handle_EditAsset(msg as EditAssetMessage);
            }
            return false;
        }
        #endregion
    }
}
